# -*- coding: utf-8 -*-
# @Time    : 2024/7/16 11:48
# @Author  : yujiahao
# @File    : 33_fastapi_safety_pw_bearer.py
# @description:OAuth2 实现简单的 Password 和 Bearer 验证


from typing import Union

from fastapi import Depends, FastAPI, HTTPException, status
# 首先，导入 OAuth2PasswordRequestForm，然后，在 /token 路径操作 中，用 Depends 把该类作为依赖项。
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel

"""
这里实现上一节欠缺的部分，实现完整的安全流：
-------------------

1. 获取 username 和 password：
    - 使用 FastAPI 安全工具获取 username 和 password。
    - OAuth2 规范要求使用密码流时，客户端或用户必须以表单数据形式发送 username 和 password 字段。
    - 这两个字段必须命名为 username 和 password，不能使用 user-name 或 email 等其它名称。
    - 前端仍可以显示终端用户所需的名称，数据库模型也可以使用所需的名称。
    - 对于登录路径操作，则要使用兼容规范的 username 和 password，以实现与 API 文档集成。
    - 规范要求必须以表单数据形式发送 username 和 password，因此，不能使用 JSON 对象。

2. Scope（作用域）：
    - OAuth2 还支持客户端发送 scope 表单字段。
    - 虽然表单字段的名称是 scope（单数），但实际上，它是以空格分隔的，由多个 scope 组成的长字符串。
    - 作用域只是声明指定权限的不带空格的字符串。
    - 常用于声明指定安全权限，例如：
        - 常见用例为 users:read 或 users:write。
        - 脸书和 Instagram 使用 instagram_basic。
        - 谷歌使用 https://www.googleapis.com/auth/drive。
    - 说明：OAuth2 中，作用域只是声明指定权限的字符串。
        - 是否使用冒号 : 等符号，或是不是 URL 并不重要。
        - 这些细节只是特定的实现方式。
        - 对 OAuth2 来说，都只是字符串而已。
"""

# 模拟数据库（数据库的密码是哈希过的）
fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "fakehashedsecret",
        "disabled": False,
    },
    "alice": {
        "username": "alice",
        "full_name": "Alice Wonderson",
        "email": "alice@example.com",
        "hashed_password": "fakehashedsecret2",
        "disabled": True,
    },
}

app = FastAPI()


# 这里对密码处理一下，模拟哈希函数（因为要和数据库的密码进行对比，所以要把传进来的数据专门哈希一下才可以对比成功）
def fake_hash_password(password: str):
    return "fakehashed" + password


# 创建一个OAuth2PasswordBearer对象，用于指定 token 的 URL
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


# 创建一个类做数据模型，表示用户的基本信息
# 根据数据分离的原则，一般不包含敏感信息，通常用于对外传递伙展示用户信息。
class User(BaseModel):
    username: str
    email: Union[str, None] = None
    full_name: Union[str, None] = None
    disabled: Union[bool, None] = None


# 继承 User 类，增加 hashed_password 字段，表示存储在数据库中的用户信息，通常内部使用
class UserInDB(User):
    hashed_password: str


# 从模拟数据库中获取用户信息，返回 UserInDB 对象
def get_user(db, username: str):
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)


# 解码 token，这里只是简单地将 token 作为用户名来处理
def fake_decode_token(token):
    user = get_user(fake_users_db, token)
    return user


"""
更新依赖项：
-------------
1. 接下来，更新依赖项。
2. 使之仅在当前用户为激活状态时，才能获取 current_user。
3. 为此，要再创建一个依赖项 get_current_active_user，此依赖项以 get_current_user 依赖项为基础。
4. 如果用户不存在，或状态为未激活，这两个依赖项都会返回 HTTP 错误。
5. 因此，在端点中，只有当用户存在、通过身份验证、且状态为激活时，才能获得该用户。
"""


# 获取当前用户信息，验证 token 的有效性
async def get_current_user(token: str = Depends(oauth2_scheme)):
    user = fake_decode_token(token)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid authentication credentials",
            headers={"WWW-Authenticate": "Bearer"},
        )
    return user


# 获取当前激活的用户，验证用户是否被禁用
async def get_current_active_user(current_user: User = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


# 获取 username 和 password 的代码
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    """
    OAuth2PasswordRequestForm：
    ---------------------------
    这是一个声明表单请求体的类依赖项，包含以下内容：

    1. 必需字段：
        - username
        - password

    2. 可选字段：
        - scope：由多个空格分隔的字符串组成的长字符串。
        - grant_type：
            - 实际上，OAuth2 规范要求 grant_type 字段使用固定值 password。
            - OAuth2PasswordRequestForm 没有作强制约束。
            - 如需强制使用固定值 password，则使用 OAuth2PasswordRequestFormStrict。
        - client_id（本例未使用）
        - client_secret（本例未使用）

    提示：
    ------
    - OAuth2PasswordRequestForm 与 OAuth2PasswordBearer 一样，都不是 FastAPI 的特殊类。
    - FastAPI 把 OAuth2PasswordBearer 识别为安全方案，因此，可以通过这种方式把它添加至 OpenAPI。
    - OAuth2PasswordRequestForm 只是可以自行编写的类依赖项，也可以直接声明 Form 参数。
    - 由于这种用例很常见，FastAPI 为了简便，直接提供了对它的支持。
    """

    user_dict = fake_users_db.get(form_data.username)
    # todo 使用表单数据
    # 现在，即可使用表单字段 username，从（伪）数据库中获取用户数据。
    # 如果不存在指定用户，则返回错误消息，提示用户名或密码错误。
    if not user_dict:
        raise HTTPException(status_code=400, detail="Incorrect username or password")
    """
    校验密码：
    -----------
    1. 从数据库中获取用户数据后，需要校验密码。
    2. 将数据放入 Pydantic 的 UserInDB 模型。
    3. 注意：永远不要保存明文密码，本例暂时先使用（伪）哈希密码系统。
    4. 如果密码不匹配，则返回错误。

    密码哈希：
    -----------
    1. 哈希是指，将指定内容（如密码）转换为形似乱码的字节序列（其实就是字符串）。
    2. 每次传入完全相同的内容（比如，完全相同的密码）时，得到的都是完全相同的乱码。
    3. 这个乱码无法转换回传入的密码。

    为什么使用密码哈希：
    ----------------------
    1. 如果数据库被盗，窃贼无法获取用户的明文密码，得到的只是哈希值。
    2. 这样一来，窃贼无法在其它应用中使用窃取的密码。
    3. 许多用户在所有系统中都使用相同的密码，使用哈希可以降低风险。
    
    
    
    关于 **user_dict：
    ------------------
    1. UserInDB(**user_dict) 的含义：
        - 直接把 user_dict 的键与值当作关键字参数传递。
        - 等效于：
            UserInDB(
                username=user_dict["username"],
                email=user_dict["email"],
                full_name=user_dict["full_name"],
                disabled=user_dict["disabled"],
                hashed_password=user_dict["hashed_password"],
            )

    """

    user = UserInDB(**user_dict)
    hashed_password = fake_hash_password(form_data.password)
    if not hashed_password == user.hashed_password:
        raise HTTPException(status_code=400, detail="Incorrect username or password")

    """
    返回 Token：
    ------------
    1. token 端点的响应必须是 JSON 对象。
    2. 响应返回的内容应该包含 token_type。
        - 本例中用的是 Bearer Token，因此，token 类型应为 "bearer"。
    3. 返回内容还应包含 access_token 字段，它是包含权限的 Token 字符串。
    4. 本例只是简单的演示，返回的 Token 就是 username，但这种方式极不安全。
    
    
    按规范的要求：
    ---------------
    1. 应像本示例一样，返回带有 access_token 和 token_type 的 JSON 对象。
    2. 这是开发者必须在代码中自行完成的工作，并且要确保使用这些 JSON 的键。
    3. 这几乎是唯一需要开发者牢记在心，并按规范要求正确执行的事。
    4. FastAPI 则负责处理其它的工作。
    """

    return {"access_token": user.username, "token_type": "bearer"}


@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    """
    关于 Bearer 的响应头 WWW-Authenticate：
    -----------------------------------------
    1. 此处返回值为 Bearer 的响应头 WWW-Authenticate 也是规范的一部分。
    2. 任何 401 UNAUTHORIZED HTTP（错误）状态码都应返回 WWW-Authenticate 响应头。
    3. 本例中，因为使用的是 Bearer Token，该响应头的值应为 "Bearer"。
    4. 实际上，忽略这个附加响应头，也不会有什么问题。
    5. 之所以在此提供这个附加响应头，是为了符合规范的要求。
    6. 说不定什么时候，就有工具用得上它，而且，开发者或用户也可能用得上。
    7. 这就是遵循标准的好处……
    """

    return current_user


"""
小结：
-------
1. 使用本章的工具实现基于 username 和 password 的完整 API 安全系统。
2. 这些工具让安全系统兼容任何数据库、用户及数据模型。
3. 唯一欠缺的是，它仍然不是真的安全。
"""
